卧槽!用代码实现 3D 冰墩墩,太浪漫了吧!
点击上方“Java精选”,选择“设为星标”
别问别人为什么,多问自己凭什么!
下方有惊喜留言必回,有问必答!
大家好,我是东哥,北京冬奥会正在如火如茶的进行中,其吉祥物冰墩墩也非常受广大朋友的喜爱。
那么你有抢到冰墩墩了么?
没抢到其实也没关系,作为一名优秀的程序员,我们也有属于自己的浪漫,实在抢不到,我们可以自己做一个网页版冰墩墩!O(∩_∩)O哈哈~
巧了!这不,Github上一位大佬dragonir,凭借自身高超的前端技术和建模技术,使用Three.js + React技术栈,自己动手创造一个充满技术含量和纪念意义的冬奥主题3D页面!
背景
迎冬奥,一起向未来!2022冬奥会马上就要开始了,本文使用 Three.js + React 技术栈,实现冬日和奥运元素,制作了一个充满趣味和纪念意义的冬奥主题 3D 页面。本文涉及到的知识点主要包括:TorusGeometry 圆环面、MeshLambertMaterial 非光泽表面材质、MeshDepthMaterial 深度网格材质、custromMaterial 自定义材质、Points 粒子、PointsMaterial 点材质等。
效果
实现效果如以下👇
动图所示,页面主要由2022
冬奥会吉祥物 冰墩墩
、奥运五环、舞动的旗帜🚩
、树木🌲
以及下雪效果❄️
等组成。按住鼠标左键移动可以改为相机位置,获得不同视图。
import React from 'react';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import bingdundunModel from './models/bingdundun.glb';
// ...
<div>
<div id="container"></div>
{this.state.loadingProcess === 100 ? '' : (
<div className="olympic_loading">
<div className="box">{this.state.loadingProcess} %</div>
</div>
)}
</div>
container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
scene = new THREE.Scene();
scene.background = new THREE.TextureLoader().load(skyTexture);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// 直射光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(16, 16, 8);
light.castShadow = true;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 40;
light.shadow.camera.bottom = -40;
light.shadow.camera.left = -40;
light.shadow.camera.right = 40;
scene.add(light);
// 环境光
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;
scene.add(ambientLight);
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {};
manager.onLoad = () => { console.log('Loading complete!')};
manager.onProgress = (url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
// 镜头补间动画
Animations.animateCamera(camera, controls, { x: 0, y: -1, z: 20 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
} else {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};
var loader = new THREE.GLTFLoader(manager);
loader.load(landModel, function (mesh) {
mesh.scene.traverse(function (child) {
if (child.isMesh) {
child.material.metalness = .1;
child.material.roughness = .8;
// 地面
if (child.name === 'Mesh_2') {
child.material.metalness = .5;
child.receiveShadow = true;
}
});
mesh.scene.rotation.y = Math.PI / 4;
mesh.scene.position.set(15, -20, 0);
mesh.scene.scale.set(.9, .9, .9);
land = mesh.scene;
scene.add(land);
});
loader.load(bingdundunModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
// 内部
if (child.name === 'oldtiger001') {
child.material.metalness = .5
child.material.roughness = .8
}
// 半透明外壳
if (child.name === 'oldtiger002') {
child.material.transparent = true;
child.material.opacity = .5
child.material.metalness = .2
child.material.roughness = 0
child.material.refractionRatio = 1
child.castShadow = true;
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(-8, -12, 0);
mesh.scene.scale.set(24, 24, 24);
scene.add(mesh.scene);
});
const fiveCycles = [
{ key: 'cycle_0', color: 0x0885c2, position: { x: -250, y: 0, z: 0 }},
{ key: 'cycle_1', color: 0x000000, position: { x: -10, y: 0, z: 5 }},
{ key: 'cycle_2', color: 0xed334e, position: { x: 230, y: 0, z: 0 }},
{ key: 'cycle_3', color: 0xfbb132, position: { x: -125, y: -100, z: -5 }},
{ key: 'cycle_4', color: 0x1c8b3c, position: { x: 115, y: -100, z: 10 }}
];
fiveCycles.map(item => {
let cycleMesh = new THREE.Mesh(new THREE.TorusGeometry(100, 10, 10, 50), new THREE.MeshLambertMaterial({
color: new THREE.Color(item.color),
side: THREE.DoubleSide
}));
cycleMesh.castShadow = true;
cycleMesh.position.set(item.position.x, item.position.y, item.position.z);
meshes.push(cycleMesh);
fiveCyclesGroup.add(cycleMesh);
});
fiveCyclesGroup.scale.set(.036, .036, .036);
fiveCyclesGroup.position.set(0, 10, -8);
scene.add(fiveCyclesGroup);
TorusGeometry(radius: Float, tube: Float, radialSegments: Integer, tubularSegments: Integer, arc: Float)
MeshLambertMaterial(parameters : Object)
loader.load(flagModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
// 旗帜
if (child.name === 'mesh_0001') {
child.material.metalness = .1;
child.material.roughness = .1;
child.material.map = new THREE.TextureLoader().load(flagTexture);
}
// 旗杆
if (child.name === '柱体') {
child.material.metalness = .6;
child.material.roughness = 0;
child.material.refractionRatio = 1;
child.material.color = new THREE.Color(0xeeeeee);
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(2, -7, -1);
mesh.scene.scale.set(4, 4, 4);
// 动画
let meshAnimation = mesh.animations[0];
mixer = new THREE.AnimationMixer(mesh.scene);
let animationClip = meshAnimation;
let clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
scene.add(mesh.scene);
});
let treeMaterial = new THREE.MeshPhysicalMaterial({
map: new THREE.TextureLoader().load(treeTexture),
transparent: true,
side: THREE.DoubleSide,
metalness: .2,
roughness: .8,
depthTest: true,
depthWrite: false,
skinning: false,
fog: false,
reflectivity: 0.1,
refractionRatio: 0,
});
let treeCustomDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: new THREE.TextureLoader().load(treeTexture),
alphaTest: 0.5
});
loader.load(treeModel, mesh => {
mesh.scene.traverse(child =>{
if (child.isMesh) {
child.material = treeMaterial;
child.custromMaterial = treeCustomDepthMaterial;
}
});
mesh.scene.position.set(14, -9, 0);
mesh.scene.scale.set(16, 16, 16);
scene.add(mesh.scene);
// 克隆另两棵树
let tree2 = mesh.scene.clone();
tree2.position.set(10, -8, -15);
tree2.scale.set(18, 18, 18);
scene.add(tree2)
// ...
});
MeshDepthMaterial(parameters: Object)
// 雪花贴图
let texture = new THREE.TextureLoader().load(snowTexture);
let geometry = new THREE.Geometry();
let range = 100;
let pointsMaterial = new THREE.PointsMaterial({
size: 1,
transparent: true,
opacity: 0.8,
map: texture,
// 背景融合
blending: THREE.AdditiveBlending,
// 景深衰弱
sizeAttenuation: true,
depthTest: false
});
for (let i = 0; i < 1500; i++) {
let vertice = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2);
// 纵向移速
vertice.velocityY = 0.1 + Math.random() / 3;
// 横向移速
vertice.velocityX = (Math.random() - 0.5) / 3;
// 加入到几何
geometry.vertices.push(vertice);
}
geometry.center();
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);
new THREE.Points(geometry, material);
PointsMaterial(parameters : Object)
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
// 禁用平移
controls.enablePan = false;
// 禁用缩放
controls.enableZoom = false;
// 垂直旋转角度限制
controls.minPolarAngle = 1.4;
controls.maxPolarAngle = 1.8;
// 水平旋转角度限制
controls.minAzimuthAngle = -.6;
controls.maxAzimuthAngle = .6;
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
controls && controls.update();
// 旗帜动画更新
mixer && mixer.update(new THREE.Clock().getDelta());
// 镜头动画
TWEEN && TWEEN.update();
// 五环自转
fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
// 顶点变动之后需要更新,否则无法实现雨滴特效
points.geometry.verticesNeedUpdate = true;
// 雪花动画更新
let vertices = points.geometry.vertices;
vertices.forEach(function (v) {
v.y = v.y - (v.velocityY);
v.x = v.x - (v.velocityX);
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
});
}
3000+ 道 BAT 大厂面试题在线刷,最新、最全 Java 面试题!
JVM 调优的几种场景,性能提升近 100 倍!如何做到的?
最近有很多人问,有没有读者交流群!想知道如何加入?方式很简单,兴趣相投的朋友,只需要点击下方卡片,回复“加群”,即可无套路入交流群!